1 module commons; 2 public import arsd.terminal : Color, ConsoleOutputType, ConsoleInputFlags; 3 public static import arsd.terminal; 4 public import std.array:join, split; 5 public import std.json; 6 public import std.path; 7 public import std.process; 8 public static import std.file; 9 public import default_handlers; 10 11 12 enum hipremeEngineRepo = "https://github.com/MrcSnm/HipremeEngine.git"; 13 enum ConfigFile = "gamebuild.json"; 14 15 JSONValue engineConfig; 16 Config configs; 17 18 string pathBeforeNewLdc; 19 20 struct Terminal 21 { 22 import std.stdio; 23 arsd.terminal.Terminal* arsdTerminal; 24 this(arsd.terminal.Terminal* arsdTerminal) 25 { 26 this.arsdTerminal = arsdTerminal; 27 } 28 29 void color(Color main, Color secondary){if(arsdTerminal) arsdTerminal.color(main, secondary);} 30 int cursorY() 31 { 32 if(arsdTerminal) return arsdTerminal.cursorY; 33 return 0; 34 } 35 string getline(string message) 36 { 37 if(arsdTerminal) return arsdTerminal.getline(message); 38 std.stdio.writeln("Can't get line with message [", message, "]"); 39 return ""; 40 } 41 void moveTo(int x, int y){if(arsdTerminal) arsdTerminal.moveTo(x, y);} 42 void clear(){if(arsdTerminal) arsdTerminal.clear();} 43 void write(T...)(T args) 44 { 45 if(arsdTerminal) arsdTerminal.write(args); 46 else std.stdio.write(args); 47 } 48 void flush() 49 { 50 if(arsdTerminal) arsdTerminal.flush(); 51 } 52 void hideCursor(){ if(arsdTerminal) arsdTerminal.hideCursor();} 53 void showCursor(){ if(arsdTerminal) arsdTerminal.showCursor();} 54 void clearToEndOfLine(){ if(arsdTerminal) arsdTerminal.clearToEndOfLine();} 55 56 void writeln(T...)(T args) 57 { 58 if (arsdTerminal) arsdTerminal.writeln(args); 59 else std.stdio.writeln(args); 60 } 61 ~this() 62 { 63 if(arsdTerminal) destroy(*arsdTerminal); 64 } 65 } 66 67 struct RealTimeConsoleInput 68 { 69 private arsd.terminal.RealTimeConsoleInput* input; 70 this(arsd.terminal.RealTimeConsoleInput* input){this.input = input;} 71 dchar getch() 72 { 73 if(input) return input.getch(); 74 return '\0'; 75 } 76 ~this() 77 { 78 if(input) destroy(*input); 79 } 80 } 81 82 struct TerminalColors 83 { 84 private Terminal* _t; 85 this(Color main, Color secondary, ref Terminal terminal) 86 { 87 _t = &terminal; 88 _t.color(main, secondary); 89 } 90 ~this() 91 { 92 _t.color(Color.DEFAULT, Color.DEFAULT); 93 } 94 } 95 96 struct WorkingDir 97 { 98 private string _currDir; 99 this(string targetDir) 100 { 101 _currDir = std.file.getcwd(); 102 std.file.chdir(targetDir); 103 } 104 ~this(){std.file.chdir(_currDir);} 105 } 106 107 enum ChoiceResult 108 { 109 None, 110 Continue, 111 Error, 112 Back, 113 } 114 115 struct Choice 116 { 117 string name; 118 ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected; 119 bool shouldTime; 120 string function() updateChoice; 121 bool scriptOnly; 122 123 this(string name, 124 ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected, 125 bool shouldTime = false, 126 string function() updateChoice = null, bool scriptOnly = false) 127 { 128 this.name = updateChoice ? updateChoice() : name; 129 this.onSelected = onSelected; 130 this.shouldTime = shouldTime; 131 this.updateChoice = updateChoice; 132 this.scriptOnly = scriptOnly; 133 } 134 135 bool opEquals(ref const Choice other) const 136 { 137 return name == other.name; 138 } 139 bool opEquals(string choiceName) const 140 { 141 return name == choiceName; 142 } 143 } 144 145 struct Config 146 { 147 JSONValue cfg; 148 149 this(JSONValue js) 150 { 151 cfg = js; 152 if(!("windows" in cfg)) cfg.object["windows"] = JSONValue(string[string].init); 153 if(!("posix" in cfg)) cfg["posix"] = JSONValue(string[string].init); 154 } 155 string toString() 156 { 157 return cfg.toPrettyString(JSONOptions.doNotEscapeSlashes); 158 } 159 160 auto opBinaryRight(string op, R)(const R rhs) const 161 if(op == "in") 162 { 163 version(Windows){return rhs in cfg["windows"];} 164 else version(Posix){return rhs in cfg["posix"];} 165 else static assert(false, "OS not supported"); 166 } 167 168 ref auto opIndexAssign(T)(T value, string obj) 169 { 170 version(Windows){return cfg["windows"][obj] = value;} 171 else version(Posix){return cfg["posix"][obj] = value;} 172 else static assert(false, "OS not supported"); 173 } 174 175 ref auto opIndex(string obj) 176 { 177 version(Windows){return cfg["windows"][obj];} 178 else version(Posix){return cfg["posix"][obj];} 179 else static assert(false, "OS not supported"); 180 } 181 } 182 183 struct CompilationOptions 184 { 185 bool skipRegistry; 186 bool dubVerbose; 187 bool force; 188 bool tempBuild; 189 string getDubOptions() const 190 { 191 string ret; 192 if(force) ret~= " --force"; 193 if(skipRegistry) ret~= " --skip-registry=all"; 194 if(tempBuild) ret~= " --temp-build"; 195 if(dubVerbose) ret~= " --verbose"; 196 return ret; 197 } 198 } 199 200 T[] unique(T)(T[] input) 201 { 202 bool[T] seen; 203 T[] ret; 204 foreach(v; input) 205 { 206 if(!(v in seen)) 207 { 208 seen[v] = true; 209 ret~= v; 210 } 211 } 212 return ret; 213 } 214 215 size_t selectChoiceBase(ref Terminal terminal, ref RealTimeConsoleInput input, Choice[] choices, 216 string selectionTitle, size_t selectedChoice = 0) 217 { 218 bool exit; 219 enum ArrowUp = 983078; 220 enum ArrowDown = 983080; 221 enum SelectionHint = "Select an option by using W/S or Arrow Up/Down and choose it by pressing Enter."; 222 terminal.clear(); 223 terminal.color(Color.DEFAULT, Color.DEFAULT); 224 terminal.writelnHighlighted(selectionTitle); 225 terminal.writeln(SelectionHint); 226 227 static void changeChoice(ref Terminal t, Choice[] choices, string title, Choice current, Choice next, int nextCursorOffset) 228 { 229 int currCursor = t.cursorY; 230 t.moveTo(0, currCursor); 231 t.clearToEndOfLine(); 232 t.write(current.name); 233 234 t.moveTo(0, currCursor+nextCursorOffset); 235 t.clearToEndOfLine(); 236 with(TerminalColors(Color.green, Color.DEFAULT, t)) 237 t.write(">> ", next.name); 238 t.flush; 239 } 240 241 static void changeChoiceClear(ref Terminal t, Choice[] choices, string title, Choice current, Choice next, int nextCursorOffset) 242 { 243 t.color(Color.DEFAULT, Color.DEFAULT); 244 t.clear(); 245 t.writelnHighlighted(title); 246 t.writeln(SelectionHint); 247 foreach(i, c; choices) 248 { 249 if(c.name == next.name) with(TerminalColors(Color.green, Color.DEFAULT, t)) 250 t.writeln(">> ", c.name); 251 else t.writeln(c.name); 252 } 253 t.flush; 254 } 255 256 int startLine = terminal.cursorY; 257 terminal.color(Color.DEFAULT, Color.DEFAULT); 258 259 foreach(i, choice; choices) 260 terminal.write(choice.name, i == choices.length - 1 ? "" : "\n"); 261 terminal.flush(); 262 263 terminal.moveTo(0, startLine + cast(int)selectedChoice); 264 terminal.hideCursor(); 265 266 267 size_t oldChoice = selectedChoice; 268 while(!exit) 269 { 270 changeChoiceClear(terminal, choices, selectionTitle, choices[oldChoice], choices[selectedChoice], cast(int)(cast(long)selectedChoice-oldChoice)); 271 oldChoice = selectedChoice; 272 CheckInput: switch(input.getch) 273 { 274 case 'w', 'W', ArrowUp: 275 selectedChoice = (selectedChoice + choices.length - 1) % choices.length; 276 break; 277 case 's', 'S', ArrowDown: 278 selectedChoice = (selectedChoice+1) % choices.length; 279 break; 280 case '\n': 281 exit = true; 282 break; 283 default: goto CheckInput; 284 } 285 } 286 terminal.moveTo(0, cast(int)startLine); 287 foreach(i; 0..choices.length) 288 terminal.moveTo(0, cast(int)(startLine+i)), terminal.clearToEndOfLine(); 289 terminal.moveTo(0, cast(int)startLine); 290 terminal.writelnSuccess(">> ", choices[selectedChoice].name); 291 292 terminal.showCursor(); 293 return selectedChoice; 294 } 295 296 string[] getProjectsAvailable() 297 { 298 import std.array; 299 import std.algorithm; 300 if(!("projectsAvailable" in configs)) 301 return []; 302 return (configs["projectsAvailable"].array.map!((JSONValue v) => v.str)).array; 303 } 304 305 string getValidPath(ref Terminal t, string pathRequired) 306 { 307 string path; 308 while(true) 309 { 310 path = t.getline(pathRequired); 311 if(std.file.exists(path)) 312 return path; 313 } 314 } 315 316 bool filesExists(string basePath, scope immutable string[] files...) 317 { 318 foreach(f; files) 319 { 320 auto temp = buildNormalizedPath(basePath, f); 321 if(!std.file.exists(temp)) return false; 322 } 323 return true; 324 } 325 326 string getFirstExisting(string basePath, scope string[] tests...) 327 { 328 foreach(t; tests) 329 { 330 auto temp = buildNormalizedPath(basePath, t); 331 if(std.file.exists(temp)) return temp; 332 } 333 return ""; 334 } 335 336 string getHipPath(scope string[] paths...) 337 { 338 return buildPath([configs["hipremeEnginePath"].str] ~ paths); 339 } 340 341 string getFirstExistingVar(scope string[] vars...) 342 { 343 foreach(variable; vars) 344 { 345 if(variable in environment) 346 return environment[variable]; 347 } 348 return ""; 349 } 350 351 352 353 bool hasLdc() 354 { 355 return ("ldcPath" in configs) !is null; 356 } 357 358 private bool dbgExecuteShell(scope const(char)[] command, ref Terminal t, const string[string] env = null) 359 { 360 auto ret = executeShell(command, env); 361 if(ret.status) 362 { 363 t.writelnError(cast(string)("Command '"~command~"' failed with: "~ ret.output)); 364 t.flush; 365 } 366 return ret.status == 0; 367 } 368 369 string findProgramPath(string program) 370 { 371 import std.algorithm:countUntil; 372 import std.process; 373 string searcher; 374 version(Windows) searcher = "where"; 375 else version(Posix) searcher = "which"; 376 else static assert(false, "No searcher program found in this OS."); 377 auto shellRes = executeShell(searcher ~" " ~ program, 378 [ 379 "PATH": environment["PATH"] 380 ]); 381 if(shellRes.status == 0) 382 return shellRes.output[0..shellRes.output.countUntil("\n")]; 383 return null; 384 } 385 386 void writelnHighlighted(ref Terminal t, scope string[] what...) 387 { 388 with(TerminalColors(Color.yellow, Color.DEFAULT, t)) 389 t.writeln(what.join()); 390 } 391 392 void writelnSuccess(ref Terminal t, scope string[] what...) 393 { 394 with(TerminalColors(Color.green, Color.DEFAULT, t)) 395 t.writeln(what.join()); 396 } 397 398 void writelnError(ref Terminal t, scope string[] what...) 399 { 400 with(TerminalColors(Color.red, Color.DEFAULT, t)) 401 t.writeln(what.join()); 402 } 403 404 auto timed(T)(scope T delegate() dg) 405 { 406 import std.datetime.stopwatch; 407 import std.stdio; 408 StopWatch sw = StopWatch(AutoStart.yes); 409 static if(is(T == void)) 410 { 411 dg(); 412 writeln(sw.peek.total!"msecs", "ms"); 413 } 414 else 415 { 416 auto ret = dg(); 417 writeln(sw.peek.total!"msecs", "ms"); 418 return ret; 419 } 420 } 421 auto timed(T)(ref Terminal t, scope T delegate() dg) 422 { 423 import std.datetime.stopwatch; 424 StopWatch sw = StopWatch(AutoStart.yes); 425 static if(is(T == void)) 426 { 427 dg(); 428 t.writeln(sw.peek.total!"msecs", "ms"); 429 } 430 else 431 { 432 auto ret = dg(); 433 t.writeln(sw.peek.total!"msecs", "ms"); 434 return ret; 435 } 436 } 437 438 439 struct Session 440 { 441 struct Cache 442 { 443 size_t line; 444 string file; 445 } 446 bool[Cache] cache; 447 } 448 private __gshared Session session; 449 450 void cached(scope void delegate() dg, string f = __FILE__, size_t l = __LINE__) 451 { 452 if(!(Session.Cache(l, f) in session.cache)) 453 { 454 session.cache[Session.Cache(l, f)] = true; 455 dg(); 456 } 457 } 458 459 /** 460 * Clears all cache. 461 * This may be useful after a dub.template.json was already generated. 462 * Or for example, after changing the current game. 463 */ 464 void clearCache() 465 { 466 session.cache.clear; 467 } 468 469 bool pollForExecutionPermission(ref Terminal t, ref RealTimeConsoleInput input, string operation) 470 { 471 t.writelnHighlighted(operation~" [Y]es/[N]o"); 472 t.flush; 473 while(true) 474 { 475 switch(input.getch) 476 { 477 case 'y', 'Y': return true; 478 case 'n', 'N': return false; 479 default: break; 480 } 481 } 482 } 483 484 bool extractZipToFolder(string zipPath, string outputDirectory, ref Terminal t) 485 { 486 import std.zip; 487 ZipArchive zip = new ZipArchive(std.file.read(zipPath)); 488 if(!std.file.exists(outputDirectory)) 489 { 490 t.writeln("Creating directory ", outputDirectory); 491 t.flush; 492 std.file.mkdirRecurse(outputDirectory); 493 } 494 foreach(fileName, archiveMember; zip.directory) 495 { 496 string outputFile = buildNormalizedPath(outputDirectory, fileName); 497 if(!std.file.exists(outputFile)) 498 { 499 if(archiveMember.expandedSize == 0) 500 std.file.mkdirRecurse(outputFile); 501 else 502 { 503 string currentDirName = outputFile; 504 ///For some reason on linux it thinks that .a files are directories 505 t.writeln("Extracting ", fileName); 506 t.flush; 507 currentDirName = currentDirName.dirName; 508 if(!std.file.exists(currentDirName)) 509 std.file.mkdirRecurse(currentDirName); 510 std.file.write(outputFile, zip.expand(archiveMember)); 511 } 512 } 513 } 514 return true; 515 } 516 517 518 bool extractToFolder(string zPath, string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input) 519 { 520 import std.path; 521 switch(zPath.extension) 522 { 523 case ".gz", ".xz": 524 version(Posix) 525 { 526 return extractTarGzToFolder(zPath, outputDirectory, t); 527 } 528 else assert(false, "No .tar.gz support on non Posix"); 529 case ".zip": 530 return extractZipToFolder(zPath, outputDirectory, t); 531 case ".7zip", ".7z": 532 return extract7ZipToFolder(zPath, outputDirectory, t, input); 533 default: 534 t.writelnError("Could not detect compressed archive type for "~zPath); 535 return false; 536 } 537 } 538 539 bool extract7ZipToFolder(string zPath, string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input) 540 { 541 if(!install7Zip("Extracting the file at"~zPath, t, input)) 542 { 543 t.writelnError("This operation requires a 7zip installation."); 544 return false; 545 } 546 if(!std.file.exists(zPath)) 547 { 548 t.writelnError("File ", zPath, " does not exists."); 549 return false; 550 } 551 t.writeln("Extracting ", zPath, " to ", outputDirectory); 552 t.flush; 553 554 string folderName = baseName(outputDirectory); 555 outputDirectory = dirName(outputDirectory); 556 if(!std.file.exists(outputDirectory)) 557 std.file.mkdirRecurse(outputDirectory); 558 559 with(WorkingDir(outputDirectory)) 560 { 561 bool ret = dbgExecuteShell(configs["7zip"].str ~ " x -y "~zPath~" "~folderName, t); 562 return ret; 563 } 564 } 565 566 version(Posix) 567 bool extractTarGzToFolder(string tarGzPath, string outputDirectory, ref Terminal t) 568 { 569 if(!std.file.exists(tarGzPath)) 570 { 571 t.writelnError("File ", tarGzPath, " does not exists."); 572 return false; 573 } 574 t.writeln("Extracting ", tarGzPath, " to ", outputDirectory); 575 t.flush; 576 std.file.mkdirRecurse(outputDirectory.dirName); 577 return dbgExecuteShell("tar -xf "~tarGzPath~" -C "~outputDirectory.dirName, t); 578 } 579 580 bool isRecognizedExtension(string ext) 581 { 582 switch(ext) 583 { 584 case ".7z", ".7zip", ".tar", ".xz", ".zf", ".bz", ".gz", ".zip": return true; 585 default: return false; 586 } 587 } 588 589 /** 590 * Removes the extension (while keeping numeric extensions such as dmd-2.105.0) 591 * Params: 592 * input = Input to remove extension 593 * Returns: 594 */ 595 string removeExtension(string input) 596 { 597 import std.string:isNumeric; 598 string ext; 599 while((ext = input.extension).length && ext.isRecognizedExtension) 600 input = input.setExtension(""); 601 return input; 602 } 603 604 /** 605 * 606 * Params: 607 * purpose = A message for the user to understand what is happening 608 * link = The link to file which will be downloaded to a temp dir 609 * outputName = A file name with a compressed archive extension (e.g: .zip, .7z, .tar.xz) 610 * outputDirectory = Where the file from outputName will be extracted 611 * t = Terminal 612 * input = RealTimeInput 613 * Returns: 614 */ 615 bool installFileTo(string purpose, string link, string outputName, 616 string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input) 617 { 618 string downloadDir = buildNormalizedPath(std.file.tempDir, outputName); 619 if(!downloadFileIfNotExists(purpose, link, downloadDir, t, input)) 620 { 621 t.writelnError("Download failed"); 622 t.flush; 623 return false; 624 } 625 626 627 outputName = outputName.removeExtension; 628 629 string installDir = buildNormalizedPath(outputDirectory, outputName); 630 if(!extractToFolder(downloadDir, installDir, t, input)) 631 { 632 t.writelnError("Could not extract ",downloadDir, " to ", installDir); 633 return false; 634 } 635 636 return true; 637 } 638 639 bool makeFileExecutable(string filePath) 640 { 641 version(Windows) return true; 642 version(Posix) 643 { 644 if(!std.file.exists(filePath)) return false; 645 import std.conv:octal; 646 std.file.setAttributes(filePath, octal!700); 647 return true; 648 } 649 } 650 651 bool downloadFileIfNotExists( 652 string purpose, string link, string outputName, 653 ref Terminal t, ref RealTimeConsoleInput input 654 ) 655 { 656 import std.net.curl; 657 import std.conv:to; 658 string theDir = dirName(outputName); 659 if(!std.file.exists(theDir)) 660 std.file.mkdirRecurse(theDir); 661 if(!std.file.exists(outputName)) 662 { 663 if(!pollForExecutionPermission(t, input, "Your system will download a file: "~ purpose~"("~link~")")) 664 return false; 665 t.writelnHighlighted("Download started."); 666 t.flush; 667 size_t time = downloadWithProgressBar(t, link, outputName); 668 t.writelnSuccess("\nDownload succeeded after ", time.to!string, " msecs!"); 669 t.flush; 670 } 671 return true; 672 } 673 674 private void terminalProgressBar(ref Terminal t, float percentage, ubyte ticksCount = 32) 675 { 676 assert(percentage <= 1.0 && percentage >= 0, "Invalid percentage."); 677 678 ubyte drawnTicks = cast(ubyte)(ticksCount*percentage); 679 int line = t.cursorY; 680 t.moveTo(0, line); 681 t.clearToEndOfLine(); 682 t.write("<"); 683 foreach(int i; 0..ticksCount) 684 { 685 t.color(i < drawnTicks ? Color.green : Color.red, Color.DEFAULT); 686 t.write(i < drawnTicks ? "=" : "."); 687 } 688 t.color(Color.DEFAULT, Color.DEFAULT); 689 t.write("> (", percentage*100, "%)"); 690 t.flush(); 691 } 692 693 /** 694 * Same as std.net.curl.download 695 * Difference is that it shows a progress bar while downloading. 696 * Returns the time needed to download. 697 */ 698 size_t downloadWithProgressBar(ref Terminal t, string url, string saveToPath, size_t updateDelay = 125) 699 { 700 import std.net.curl:HTTP; 701 import core.time:dur; 702 import std.datetime.stopwatch:StopWatch, AutoStart; 703 import std.stdio : File; 704 size_t received, contentLength; 705 HTTP conn = HTTP(); 706 conn.url = url; 707 static void writer(string path) 708 { 709 auto f = File(path, "wb"); 710 while(true) 711 { 712 immutable(ubyte)[] data = receiveOnly!(immutable(ubyte)[]); 713 if(data.length == 0) 714 break; 715 f.rawWrite(data); 716 } 717 ownerTid.send(true); 718 } 719 auto writerTid = spawn(&writer, saveToPath); 720 t.hideCursor(); 721 StopWatch sw = StopWatch(AutoStart.yes); 722 size_t downloadTime; 723 conn.onReceive = (ubyte[] data) 724 { 725 import std.conv:to; 726 if(contentLength == 0) 727 contentLength = conn.responseHeaders["content-length"].to!size_t; 728 received+= data.length; 729 if(sw.peek.total!"msecs" >= updateDelay || received == contentLength) 730 { 731 downloadTime+= sw.peek.total!"msecs"; 732 terminalProgressBar(t, cast(float)received/contentLength); 733 sw.reset(); 734 } 735 send(writerTid, data.idup); 736 return data.length; 737 }; 738 conn.perform(); 739 send(writerTid, (immutable(ubyte)[]).init); 740 receiveTimeout(dur!"msecs"(1000), (bool){}); //Block until finish 741 t.showCursor(); 742 return downloadTime; 743 } 744 745 746 private string getConfigPath() 747 { 748 import core.runtime; 749 static string cfgPath; 750 if(cfgPath == "") 751 cfgPath = buildNormalizedPath(Runtime.args[0].dirName, ConfigFile); 752 return cfgPath; 753 } 754 private string getEngineConfigPath() 755 { 756 return getHipPath("bin" ,"desktop", "engine_opts.json"); 757 } 758 void updateEngineFile() 759 { 760 std.file.write(getEngineConfigPath, engineConfig.toPrettyString()); 761 } 762 void updateConfigFile() 763 { 764 std.file.write(getConfigPath, configs.toString()); 765 } 766 string getSourceCodeEditor(string projectPath) 767 { 768 if(!("sourceCodeEditor" in configs) || configs["sourceCodeEditor"].str.length == 0) 769 { 770 string out_Editor; 771 if(getDefaultSourceEditor(buildNormalizedPath(projectPath, "source", "gamescript", "entry.d"), out_Editor)) 772 configs["sourceCodeEditor"] = out_Editor; 773 else 774 configs["sourceCodeEditor"] = ""; 775 updateConfigFile(); 776 } 777 778 return configs["sourceCodeEditor"].str; 779 } 780 781 bool openSourceCodeEditor(string projectPath) 782 { 783 string sourceEditor = getSourceCodeEditor(projectPath); 784 if(!sourceEditor.length) 785 return false; 786 787 return executeShell(sourceEditor.escapeShellCommand~" "~projectPath.escapeShellCommand).status == 0; 788 } 789 790 791 string getGitExec() 792 { 793 if("git" in configs) 794 { 795 version(Windows) return buildNormalizedPath(configs["git"].str, "git.exe"); 796 else return buildNormalizedPath(configs["git"].str, "git"); 797 } 798 return "git "; 799 } 800 801 bool hasGit() 802 { 803 if(findProgramPath("git")) return true; 804 return ("git" in configs) != null; 805 } 806 807 void loadSubmodules(ref Terminal t, ref RealTimeConsoleInput input) 808 { 809 import std.process; 810 if(!hasGit) 811 { 812 if(!installGit(t, input)) 813 throw new Error("Git wasn't found. Git is necessary for loading the engine submodules."); 814 } 815 t.writelnSuccess("Updating Git Submodules"); 816 t.flush; 817 818 executeShell("cd "~ configs["hipremeEnginePath"].str ~ " && " ~ getGitExec~" submodule update --init --recursive"); 819 } 820 821 private bool install7Zip(string purpose, ref Terminal t, ref RealTimeConsoleInput input) 822 { 823 if(!("7zip" in configs)) 824 { 825 version(Windows) 826 { 827 string _7zPath = findProgramPath("7z"); 828 if(!_7zPath) 829 { 830 if(!downloadFileIfNotExists("Needs 7zip for "~purpose, "https://www.7-zip.org/a/7zr.exe", 831 buildNormalizedPath(std.file.getcwd(), "7z.exe"), t, input 832 )) 833 return false; 834 835 string outFolder = buildNormalizedPath(std.file.getcwd(), "buildtools"); 836 std.file.mkdirRecurse(outFolder); 837 std.file.rename(buildNormalizedPath(std.file.getcwd(), "7z.exe"), buildNormalizedPath(outFolder, "7z.exe")); 838 configs["7zip"] = buildNormalizedPath(outFolder, "7z.exe"); 839 } 840 else 841 configs["7zip"] = buildNormalizedPath(_7zPath); 842 updateConfigFile(); 843 } 844 else version(Posix) 845 { 846 configs["7zip"] = "7za"; 847 updateConfigFile(); 848 } 849 } 850 return true; 851 } 852 853 854 private string getGitDownloadLink() 855 { 856 version(Windows) return "https://github.com/git-for-windows/git/releases/download/v2.40.1.windows.1/MinGit-2.40.1-64-bit.zip"; 857 else return ""; 858 } 859 860 861 private ChoiceResult _backFn(Choice* c, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions cOpts) 862 { 863 return ChoiceResult.Back; 864 } 865 Choice getBackChoice() 866 { 867 return Choice("Back", &_backFn); 868 } 869 870 871 bool installGit(ref Terminal t, ref RealTimeConsoleInput input) 872 { 873 version(Windows) 874 { 875 if(!("git" in configs)) 876 { 877 string gitPath = buildNormalizedPath(std.file.getcwd(), "buildtools", "git"); 878 if(!installFileTo("Download Git for getting HipremeEngine's source code.", getGitDownloadLink(), "git.zip", 879 gitPath, t, input)) 880 { 881 t.writelnError("Git installation failed"); 882 return false; 883 } 884 configs["git"] = buildNormalizedPath(gitPath, "cmd"); 885 updateConfigFile(); 886 } 887 return true; 888 } 889 else version(Posix) 890 { 891 t.writelnError("Please install Git to use build_selector."); 892 return false; 893 } 894 } 895 896 897 void runEngineDScript(ref Terminal t, string script, scope string[] args...) 898 { 899 import std.array; 900 import std.datetime.stopwatch; 901 StopWatch sw = StopWatch(AutoStart.yes); 902 t.writeln("Executing engine script ", script, " with arguments ", args); 903 t.flush; 904 auto exec = executeShell(configs["rdmdPath"].str ~ " " ~ buildNormalizedPath(configs["hipremeEnginePath"].str, "tools", "build", script)~" " ~ args.join(" "), 905 environment.toAA); 906 t.writeln(" Finished in ", sw.peek.total!"msecs", "ms"); 907 t.writeln(exec.output); 908 t.flush; 909 if(exec.status) 910 { 911 t.writelnError("Script ", script, " failed with: ", exec.output); 912 t.flush; 913 throw new Error("Failed on engine script"); 914 } 915 } 916 917 string getDubPath() 918 { 919 string dub = buildNormalizedPath(configs["dubPath"].str, "dub"); 920 version(Windows) dub = dub.setExtension("exe"); 921 return dub; 922 } 923 924 private int execDubBase(ref Terminal t, in DubArguments dArgs) 925 { 926 import std.conv:to; 927 if(absolutePath(configs["hipremeEnginePath"].str) != absolutePath(std.file.getcwd())) 928 if(std.file.exists("dub.template.json")) 929 { 930 import template_processor; 931 string out_DubFile; 932 auto res = processTemplate(std.file.getcwd(), configs["hipremeEnginePath"].str, out_DubFile); 933 if(res != TemplateProcessorResult.success) 934 { 935 t.writelnError(res.to!string, ":", out_DubFile); 936 return -1; 937 } 938 try std.file.write("dub.json", out_DubFile); 939 catch(Exception e){ 940 t.writelnError("Could not write dub.json"); 941 return -1; 942 } 943 } 944 return 0; 945 } 946 947 948 mixin template BuilderPattern(Struct) 949 { 950 static foreach(mem; __traits(allMembers, Struct)) 951 { 952 import std.traits:isFunction; 953 static if(!isFunction!(__traits(getMember, Struct, mem)) && mem[0] == '_') 954 { 955 mixin(typeof(__traits(getMember, Struct, mem)), " ", mem[1..$], "() { return ", mem, ";}", 956 Struct, " ", mem[1..$], "(", typeof(__traits(getMember, Struct, mem)), " arg )", 957 "{this.",mem, " = arg; return this;}"); 958 } 959 } 960 } 961 962 963 immutable string[] compilers = ["auto", "ldc2", "dmd"]; 964 string getSelectedCompiler() 965 { 966 const(JSONValue)* c = "selectedCompiler" in configs; 967 if(!c) return "auto"; 968 return compilers[c.get!uint]; 969 } 970 971 972 struct DubArguments 973 { 974 string _command; 975 string _configuration; 976 CompilationOptions _opts; 977 string _dir; 978 string _preCommands; 979 string _compiler = "auto"; 980 string _arch; 981 string _build; 982 string _recipe; 983 string _runArgs; 984 bool _confirmKey; 985 bool _deep; 986 bool _parallel = true; 987 988 mixin BuilderPattern!(DubArguments); 989 990 string getDubRunCommand() 991 { 992 string dub = getDubPath(); 993 string a = command; ///Arguments 994 if(compiler == "auto") 995 { 996 compiler = arch ? "ldc2" : getSelectedCompiler(); 997 compiler = compiler == "auto" ? "" : compiler; 998 } 999 1000 if(parallel) a~= " --parallel"; 1001 if(recipe) a~= " --recipe="~recipe; 1002 if(build) a~= " --build="~build; 1003 if(arch) a~= " --arch="~arch; 1004 if(compiler != "")a~= " --compiler="~compiler; 1005 if(deep) a~= " --deep"; 1006 if(configuration) a~= " -c "~configuration; 1007 if(opts != CompilationOptions.init) a~= opts.getDubOptions(); 1008 if(runArgs) a~= " -- "~runArgs; 1009 1010 1011 version(Windows) 1012 { 1013 if(confirmKey) a~= " && pause"; 1014 } 1015 else version(Posix) 1016 { 1017 if(confirmKey) a~= " && read -p \"Press any key to continue... \" -n1 -s"; 1018 } 1019 1020 1021 return preCommands~dub~" "~a; 1022 } 1023 } 1024 1025 int waitDub(ref Terminal t, DubArguments dArgs) 1026 { 1027 ///Detects the presence of a template file before executing. 1028 if(execDubBase(t, dArgs) == -1) return -1; 1029 string toExec = dArgs.getDubRunCommand(); 1030 t.writeln(toExec); 1031 t.flush; 1032 return wait(spawnShell(toExec)); 1033 } 1034 1035 int execDub(ref Terminal t, DubArguments dArgs) 1036 { 1037 import std.string:lineSplitter; 1038 if(execDubBase(t, dArgs) == -1) return -1; 1039 string toExec = dArgs.getDubRunCommand(); 1040 t.writeln(toExec); 1041 t.flush; 1042 auto res = executeShell(toExec, null, std.process.Config.none, size_t.max, dArgs.dir); 1043 foreach(l; res.output.lineSplitter) t.writeln("\t", l); 1044 return res.status; 1045 } 1046 1047 1048 int waitDubTarget(ref Terminal t, string target, DubArguments dArgs) 1049 { 1050 return waitDub(t, dArgs.recipe(buildPath(getBuildTarget(target), "dub.json"))); 1051 } 1052 1053 int waitAndPrint(ref Terminal t, Pid pid) 1054 { 1055 return wait(pid); 1056 } 1057 1058 public import std.concurrency; 1059 bool waitOperations(immutable bool delegate()[] operations) 1060 { 1061 foreach(op; operations) 1062 { 1063 spawn((bool delegate() targetOperation) 1064 { 1065 ownerTid.send(targetOperation()); 1066 }, op); 1067 } 1068 1069 foreach(i; 0..operations.length) 1070 if(!receiveOnly!bool) 1071 return false; 1072 return true; 1073 } 1074 1075 1076 void putResourcesIn(ref Terminal t, string where) 1077 { 1078 runEngineDScript(t, "copyresources.d", buildNormalizedPath(configs["gamePath"].str, "assets"), where); 1079 } 1080 1081 1082 1083 string selectInFolder(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input, 1084 scope string[] extFilters = [".DS_Store"]) 1085 { 1086 import std.string; 1087 Choice[] choices; 1088 LISTING_FILE: foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow)) 1089 { 1090 foreach(f; extFilters) 1091 if(e.name.endsWith(f)) continue LISTING_FILE; 1092 choices~= Choice(e.name, null); 1093 } 1094 size_t choice; 1095 choice = selectChoiceBase(t, input, choices, selectWhat); 1096 1097 return choices[choice].name; 1098 } 1099 1100 /** 1101 * Main difference from selectInFolder is that it returns the choice and also acacepts extra choices. 1102 * Params: 1103 * selectWhat = Description 1104 * directory = Directory to iterate 1105 * t = 1106 * input = 1107 * extraChoices = May be used to go back or cancel process 1108 * Returns: Selected choice 1109 */ 1110 Choice* selectInFolderExtra(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input, 1111 return scope Choice[] choices, scope Choice[] extraChoices, scope string[] extFilters = [".DS_Store"]) 1112 { 1113 import std.string; 1114 LISTING_FILES: 1115 foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow)) 1116 { 1117 foreach(f; extFilters) if(e.name.endsWith(f)) continue LISTING_FILES; 1118 choices~= Choice(e.name, null); 1119 } 1120 choices = (choices ~ extraChoices).unique; 1121 size_t choice; 1122 choice = selectChoiceBase(t, input, choices, selectWhat); 1123 1124 return &choices[choice]; 1125 } 1126 1127 1128 1129 version(Windows) 1130 { 1131 import std.windows.registry; 1132 Key windowsGetKeyWithPath(string[] path...) 1133 { 1134 Key hklm = Registry.localMachine; 1135 if(hklm is null) throw new Error("No HKEY_LOCAL_MACHINE in this system."); 1136 Key currKey = hklm; 1137 foreach(p; path) 1138 { 1139 try{ 1140 currKey = currKey.getKey(p); 1141 if(currKey is null) return null; 1142 } 1143 catch(Exception e) 1144 { 1145 return null; 1146 } 1147 } 1148 return currKey; 1149 } 1150 } 1151 1152 string getBuildTarget(string target = __MODULE__) 1153 { 1154 import std.string:split; 1155 import std.exception:enforce; 1156 target = target.split(".")[$-1]; 1157 string path = getHipPath("tools", "build", "targets"); 1158 enforce(std.file.exists(path = buildPath(path, target)), "Target "~target~" does not exists."); 1159 return path; 1160 } 1161 1162 void outputTemplate(ref Terminal t, string templatePath) 1163 { 1164 import template_processor; 1165 string out_templ; 1166 1167 switch(processTemplate(templatePath, configs["hipremeEnginePath"].str, out_templ, [ 1168 "TARGET_PROJECT": configs["gamePath"].str 1169 ])) 1170 { 1171 case TemplateProcessorResult.invalid: 1172 t.writelnError("Could not process template from path ",templatePath); 1173 throw new Error("Can't build with invalid template."); 1174 case TemplateProcessorResult.notFound: 1175 t.writelnHighlighted("Template at ", templatePath, " not found, your game may use dub.json instead."); 1176 break; 1177 default: case TemplateProcessorResult.success: 1178 t.writelnSuccess("Template at path ", templatePath, " successfully generated"); 1179 std.file.write(buildPath(templatePath, "dub.json"), out_templ); 1180 break; 1181 } 1182 } 1183 1184 void outputTemplateForTarget(ref Terminal t, string target = __MODULE__) 1185 { 1186 import std.array:split; 1187 ///If it is the default, the target will be "targets.wasm", so, split and get the last. 1188 string buildTarget = getBuildTarget(target.split(".")[$-1]); 1189 t.writeln("Regenerating buildscript for target ", buildTarget); 1190 outputTemplate(t, buildTarget); 1191 } 1192 1193 void requireConfiguration(string cfgRequired, string purpose, ref Terminal t, ref RealTimeConsoleInput input) 1194 { 1195 if(!(cfgRequired in configs)) 1196 { 1197 configs[cfgRequired] = t.getline("Config '"~cfgRequired~"' is required for "~ purpose~ ". \n\tWrite here: "); 1198 updateConfigFile(); 1199 } 1200 } 1201 1202 /** 1203 * 1204 * Params: 1205 * original = The original path where the link will redirect 1206 * link = The path where the link will be created 1207 */ 1208 void symlink(string original, string link) 1209 { 1210 version(Posix){ 1211 std.file.symlink(original, link); 1212 } 1213 version(Windows) 1214 { 1215 import core.sys.windows.w32api:_WIN32_WINNT; 1216 static if(_WIN32_WINNT >= 0x600) //WindowsVista or later 1217 { 1218 import core.sys.windows.winbase; 1219 import core.sys.windows.windef:DWORD, MAX_PATH, LPWSTR; 1220 import std.utf:toUTF16z; 1221 import std.file:FileException; 1222 1223 DWORD typeFlag = 0; //File 1224 if(std.file.isDir(original)) 1225 typeFlag = SYMBOLIC_LINK_FLAG_DIRECTORY; 1226 typeFlag|= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; 1227 1228 if(link.length > MAX_PATH) link = `\\?\`~link; 1229 if(original.length > MAX_PATH) original = `\\?\`~original; 1230 1231 if(!CreateSymbolicLinkW(link.toUTF16z, original.toUTF16z, typeFlag)) 1232 { 1233 LPWSTR strBuffer; 1234 DWORD length = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, null, GetLastError(),0, cast(LPWSTR)&strBuffer, 0, null); 1235 wchar[] str = new wchar[length]; 1236 str[] = strBuffer[0..str.length]; 1237 LocalFree(strBuffer); 1238 import std.conv; 1239 throw new FileException(original, str.to!string); 1240 } 1241 } 1242 } 1243 } 1244 1245 1246 /** 1247 * May be used in future. Kept for reference. 1248 */ 1249 private bool hasAdminRights() 1250 { 1251 version(Windows) 1252 { 1253 ///https://stackoverflow.com/questions/8046097/how-to-check-if-a-process-has-the-administrative-rights 1254 import core.sys.windows.windows; 1255 bool hasRights = false; 1256 HANDLE hToken = NULL; 1257 if( OpenProcessToken( GetCurrentProcess( ),TOKEN_QUERY,&hToken ) ) { 1258 TOKEN_ELEVATION Elevation; 1259 DWORD cbSize = TOKEN_ELEVATION.sizeof; 1260 if(GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenElevation, &Elevation, Elevation.sizeof, &cbSize)) 1261 hasRights = Elevation.TokenIsElevated == 1; 1262 } 1263 if(hToken) CloseHandle(hToken); 1264 return hasRights; 1265 } 1266 else return false; 1267 } 1268 1269 1270 static this() 1271 { 1272 configs = std.file.exists(getConfigPath) ? Config(parseJSON(std.file.readText(getConfigPath))) : Config(parseJSON("{}")); 1273 try engineConfig = std.file.exists(getEngineConfigPath) ? parseJSON(std.file.readText(getEngineConfigPath)) : parseJSON("{}"); 1274 catch(Exception e) engineConfig = parseJSON("{}"); 1275 }